package ai.neuralnet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class NeuralNetwork<N extends TNeuron> implements Comparable<NeuralNetwork>
{

    /**
     * accuracy score for optimization
     * can be set and get externally
     * to avoid recomputation for no reason
     */
    private double accScore = 0;


    private NNBuilder builder;

    private N[] inputNeurons;


    protected N[] outputNeurons;


    protected ArrayList<N> hiddenNeurons;


    /**
     * Randomizes weights of all hidden and output neurons to real values
     * indside the specified range.
     * Used for Initialization
     *
     * @param minValue minimum allowed weight
     * @param maxValue maximum allowed weight
     */
    public void randomizeWeights(double minValue, double maxValue)
    {
        for (N n : hiddenNeurons)
        {
            n.randomizeWeights(minValue, maxValue);
        }
        for (N n : outputNeurons)
        {
            n.randomizeWeights(minValue, maxValue);
        }

    }


    public void propagateInputs(double[] inputs)
    {

        //reset the inputSet on the outputs in case of previous pass
        for (int i = 0; i < outputNeurons.length; i++)
        {
            outputNeurons[i].resetInputSet();
        }

        for (int i = 0; i < inputNeurons.length; i++)
        {
            inputNeurons[i].setInput(0, inputs[i]);
            inputNeurons[i].tryPropagateForward();
        }

        while (!outputLayerReady())
        {
            // System.out.println("Propagating through hidden layer");
            for (N n : hiddenNeurons)
            {
                n.tryPropagateForward();
            }
        }
    }

    public void setWeights(double weight)
    {
        for (N n : hiddenNeurons)
        {
            n.setWeights(weight);
        }

        for (N n : outputNeurons)
        {
            n.setWeights(weight);
        }
    }


    public double[] getOutputs()
    {
        double[] out = new double[outputNeurons.length];
        for (int i = 0; i < outputNeurons.length; i++)
        {
            out[i] = outputNeurons[i].computeOutput();
        }

        return out;

    }

    private boolean outputLayerReady()
    {
        boolean ready = true;
        for (int i = 0; i < outputNeurons.length; i++)
        {
            ready &= outputNeurons[i].allInputsReady();
            if (!ready) break;
        }
        return ready;
    }

    public NeuralNetwork<N> clone()
    {
        NeuralNetwork<N> network = builder.build();

        for (int i = 0; i < hiddenNeurons.size(); i++)
        {
            network.hiddenNeurons.get(i).setWeights(hiddenNeurons.get(i).weights);
        }

        for (int i = 0; i < outputNeurons.length; i++)
        {
            network.outputNeurons[i].setWeights(outputNeurons[i].weights);
        }

        return network;
    }

    /**
     * will create a new network with the specified builder
     * and then copy weights, it is important that
     * the builder provided have the same underlying
     * type as the original one (minus type parameters)
     * <p/>
     * ie both must be ConnectedMultilayerBuilders with the same dimension
     * parameters, but one could have been making a gradient descent network
     * and the other could have been for a genetic network
     *
     * @param builder builder with same topology but different type than the original
     * @return new network with same topology and weights but of different type
     */
    public NeuralNetwork transform(NNBuilder builder)
    {
        NeuralNetwork network = builder.build();

        for (int i = 0; i < hiddenNeurons.size(); i++)
        {
            ((TNeuron) network.hiddenNeurons.get(i)).setWeights(hiddenNeurons.get(i).weights);
        }

        for (int i = 0; i < outputNeurons.length; i++)
        {
            network.outputNeurons[i].setWeights(outputNeurons[i].weights);
        }

        return network;
    }

    public NeuralNetwork(NNBuilder builder)
    {
        this.builder = builder;
        hiddenNeurons = new ArrayList<N>();
    }

    public void addHiddenNeuron(N n)
    {
        hiddenNeurons.add(n);
    }

    public void addHiddenNeuron(Collection<N> neurons)
    {
        hiddenNeurons.addAll(neurons);
    }


    public N[] getInputNeurons()
    {
        return inputNeurons;
    }

    public void setInputNeurons(N[] inputNeurons)
    {
        this.inputNeurons = inputNeurons;
    }

    public N[] getOutputNeurons()
    {
        return outputNeurons;
    }

    public void setOutputNeurons(N[] outputNeurons)
    {
        this.outputNeurons = outputNeurons;
    }

    public double getAccScore()
    {
        return accScore;
    }

    public void setAccScore(double accScore)
    {
        this.accScore = accScore;
    }


    public List<Double> getWeights()
    {
        List<Double> weights = new ArrayList<Double>();
        for (TNeuron neuron : hiddenNeurons)
        {
            for (double w : neuron.getWeights())
            {
                weights.add(w);
            }
        }

        for (TNeuron neuron : outputNeurons)
        {
            for (double w : neuron.getWeights())
            {
                weights.add(w);
            }
        }

        return weights;
    }


    public void setWeights(List<Double> weights)
    {
        int wc = 0;

        for (TNeuron neuron : hiddenNeurons)
        {
            double[] nweights = neuron.getWeights();
            for (int i = 0; i < nweights.length; i++)
            {
                nweights[i] = weights.get(wc++);
            }
        }

        for (TNeuron neuron : outputNeurons)
        {
            double[] nweights = neuron.getWeights();
            for (int i = 0; i < nweights.length; i++)
            {
                nweights[i] = weights.get(wc++);
            }
        }

    }

    @Override
    public int compareTo(NeuralNetwork o)
    {
        double diff = o.getAccScore() - getAccScore();
        return diff < 0 ? -1 : diff > 0 ? 1 : 0;
    }
}
